package t4tools.lepath.generate;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import t4tools.lepath.generate.visitor.Reader;
import t4tools.lepath.model.Clazz;
import t4tools.lepath.util.FieldByFieldNameHashCodeComparator;
import t4tools.lepath.util.GeneratorUtil;

public class Generator {

	public static byte[] generateFor(Class<?> clazz) throws IOException {
		// 1st gather info
		// TODO: there's probably a way to that and generate at the same time
		Reader r = new Reader(clazz);
		ClassReader cr = new ClassReader(clazz.getName());

		cr.accept(r, 0);

		// 2nd generate property getter class
		return Generator.generateClazz(r.getClazz());
	}

	private static byte[] generateClazz(Clazz info) {
		ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

		String clazzSignature = "Ljava/lang/Object;Lt4tools/lepath/generate/PropertyGetter<" + Type.getDescriptor(info.getTarget()) + ">;";
		writer.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, info.getName(), clazzSignature, Type.getInternalName(Object.class), 
				new String[] { Type.getInternalName(PropertyGetter.class) });

		GeneratorUtil.addDefaultConstruct(writer);
		Generator.implementGetMethod(writer, info);

		writer.visitEnd();
		return writer.toByteArray();
	}

	private static void implementGetMethod(ClassWriter writer, Clazz info) {

		Collections.sort(info.getFields(), new FieldByFieldNameHashCodeComparator());
		int[] hashes = new int[info.getFields().size()];
		Label[] labels = new Label[info.getFields().size()];
		for (int i = 0; i < hashes.length; i++) {
			hashes[i] = info.getFields().get(i).getName().hashCode();
			labels[i] = new Label();
		}

		MethodVisitor mv = writer.visitMethod(
				Opcodes.ACC_PUBLIC, "get", "(" + Type.getDescriptor(String.class) + Type.getDescriptor(info.getTarget()) + ")"
						+ Type.getDescriptor(Object.class), null, null);
		mv.visitCode();
		mv.visitVarInsn(Opcodes.ALOAD, 1);
		mv.visitInsn(Opcodes.DUP);
		mv.visitVarInsn(Opcodes.ASTORE, 3);
		mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(String.class), "hashCode", "()I");
		Label defaultCase = new Label();
		mv.visitLookupSwitchInsn(defaultCase, hashes, labels);

		List<Label> returnLabels = new ArrayList<Label>();

		for (int j = 0; j < hashes.length; j++) {
			mv.visitLabel(labels[j]);
			mv.visitVarInsn(Opcodes.ALOAD, 3);
			mv.visitLdcInsn(info.getFields().get(j).getName());
			mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(String.class), "equals", "(" + Type.getDescriptor(Object.class) + ")Z");

			Label returnLabel = new Label();
			mv.visitJumpInsn(Opcodes.IFNE, returnLabel);
			mv.visitJumpInsn(Opcodes.GOTO, defaultCase);
			returnLabels.add(returnLabel);
			mv.visitLabel(returnLabel);
			mv.visitVarInsn(Opcodes.ALOAD, 2);
			mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(info.getTarget()), info.getFields().get(j).getGetter(), "()"
					+ info.getFields().get(j).getTypeDescr());
			GeneratorUtil.boxPrimitive(mv, info.getFields().get(j));
			mv.visitInsn(Opcodes.ARETURN);
		}

		mv.visitLabel(defaultCase);
		mv.visitInsn(Opcodes.ACONST_NULL);
		mv.visitInsn(Opcodes.ARETURN);
		mv.visitMaxs(1, 1);
		mv.visitEnd();

		Generator.addBridgeMethod(writer, info);
	}

	private static void addBridgeMethod(ClassWriter writer, Clazz info) {

		MethodVisitor mv = writer.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_BRIDGE + Opcodes.ACC_SYNTHETIC, "get",
				"(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;", null, null);
		mv.visitCode();
		mv.visitVarInsn(Opcodes.ALOAD, 0);
		mv.visitVarInsn(Opcodes.ALOAD, 1);
		mv.visitVarInsn(Opcodes.ALOAD, 2);
		String targetInternal = Type.getInternalName(info.getTarget());
		mv.visitTypeInsn(Opcodes.CHECKCAST, targetInternal);
		mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, info.getName(), "get",
				"(Ljava/lang/String;" + Type.getDescriptor(info.getTarget()) + ")Ljava/lang/Object;");
		mv.visitInsn(Opcodes.ARETURN);
		mv.visitMaxs(3, 3);
		mv.visitEnd();
	}
}
